﻿import Config from "./mapConfig";
const url = import.meta.env.VITE_backurl
const url_gg=import.meta.env.VITE_backurl_gg

if (!L.DomUtil.setTransform) {
  L.DomUtil.setTransform = function (el, offset, scale) {
    var pos = offset || new L.Point(0, 0);
    el.style[L.DomUtil.TRANSFORM] = (L.Browser.ie3d ? 'translate(' + pos.x + 'px,' + pos.y + 'px)' : 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + (scale ? ' scale(' + scale + ')' : '');
  };
}

L.CanvasLayer = (L.Layer ? L.Layer : L.Class).extend({

  initialize: function initialize(options) {
    this._map = null;
    this._canvas = null;
    this._frame = null;
    this._delegate = null;
    this.zindex = null;
    L.setOptions(this, options);
  },

  delegate: function delegate(del) {
    this._delegate = del;
    return this;
  },

  needRedraw: function needRedraw() {
    if (!this._frame) {
      this._frame = L.Util.requestAnimFrame(this.drawLayer, this);
    }
    return this;
  },

  _onLayerDidResize: function _onLayerDidResize(resizeEvent) {
    this._canvas.width = resizeEvent.newSize.x * this.options.magnification;
    this._canvas.height = resizeEvent.newSize.y * this.options.magnification;
    this._canvas.style.width = resizeEvent.newSize.x + "px";
    this._canvas.style.height = resizeEvent.newSize.y + "px";
  },

  _onLayerDidMove: function () {
    if (!this._map) return
    var topLeft = this._map.containerPointToLayerPoint([0, 0]);
    L.DomUtil.setPosition(this._canvas, topLeft);
    this.drawLayer();
  },

  getEvents: function getEvents() {
    var events = {
      resize: this._onLayerDidResize,
      moveend: this._onLayerDidMove
    };
    if (this._map.options.zoomAnimation && L.Browser.any3d) {
      events.zoomanim = this._animateZoom;
    }
    return events;
  },

  onAdd: function onAdd(map) {
    this._map = map;
    this._canvas = L.DomUtil.create('canvas', 'leaflet-layer');
    this.tiles = {};
    this._canvas.style.zIndex = this.options.zindex;
    this._canvas.style.pointerEvents = "none";
    var size = this._map.getSize();
    this._canvas.width = size.x * this.options.magnification;
    this._canvas.height = size.y * this.options.magnification;
    this._canvas.style.width = size.x + "px";
    this._canvas.style.height = size.y + "px";

    var animated = this._map.options.zoomAnimation && L.Browser.any3d;
    L.DomUtil.addClass(this._canvas, 'leaflet-zoom-' + (animated ? 'animated' : 'hide'));

    map._panes.overlayPane.appendChild(this._canvas);
    map.on(this.getEvents(), this);

    var del = this._delegate || this;
    del.onLayerDidMount && del.onLayerDidMount();
    this.needRedraw();

    var self = this;
    setTimeout(function () {
      self._onLayerDidMove();
    }, 0);
  },

  onRemove: function onRemove(map) {
    var del = this._delegate || this;
    del.onLayerWillUnmount && del.onLayerWillUnmount();
    map.getPanes().overlayPane.removeChild(this._canvas);
    map.off(this.getEvents(), this);
    this._canvas = null;
    Config.curData = {}
  },

  addTo: function addTo(map) {
    map.addLayer(this);
    return this;
  },

  LatLonToMercator: function LatLonToMercator(latlon) {
    return {
      x: latlon.lng * 6378137 * Math.PI / 180,
      y: Math.log(Math.tan((90 + latlon.lat) * Math.PI / 360)) * 6378137
    };
  },

  drawLayer: function drawLayer() {
    if (!this._map) return
    var size = this._map.getSize();
    var bounds = this._map.getBounds();
    var zoom = this._map.getZoom();
    var center = this.LatLonToMercator(this._map.getCenter());
    var corner = this.LatLonToMercator(this._map.containerPointToLatLng(this._map.getSize()));
    var del = this._delegate || this;
    del.onDrawLayer && del.onDrawLayer({
      layer: this,
      canvas: this._canvas,
      bounds: bounds,
      size: size,
      zoom: zoom,
      center: center,
      corner: corner
    });
    this._frame = null;
  },

  _setTransform: function _setTransform(el, offset, scale) {
    var pos = offset || new L.Point(0, 0);
    el.style[L.DomUtil.TRANSFORM] = (L.Browser.ie3d ? 'translate(' + pos.x + 'px,' + pos.y + 'px)' : 'translate3d(' + pos.x + 'px,' + pos.y + 'px,0)') + (scale ? ' scale(' + scale + ')' : '');
  },

  _animateZoom: function _animateZoom(e) {
    var scale = this._map.getZoomScale(e.zoom);
    var offset = L.Layer ? this._map._latLngToNewLayerPoint(this._map.getBounds().getNorthWest(), e.zoom, e.center) : this._map._getCenterOffset(e.center)._multiplyBy(-scale).subtract(this._map._getMapPanePos());
    L.DomUtil.setTransform(this._canvas, offset, scale);
  }
});

L.canvasLayer = function (options) {
  return new L.CanvasLayer(options);
};

L.Control.Velocity = L.Control.extend({

  options: {
    position: 'bottomleft',
    emptyString: 'Unavailable',
    angleConvention: 'bearingCCW',
    speedUnit: 'm/s'
  },

  onAdd: function onAdd(map) {
    this._container = L.DomUtil.create('div', 'leaflet-control-velocity');
    L.DomEvent.disableClickPropagation(this._container);
    return this._container;
  },

  onRemove: function onRemove(map) { },

  vectorToSpeed: function vectorToSpeed(uMs, vMs, unit) {
    var velocityAbs = Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2));
    if (unit === 'k/h') {
      return this.meterSec2kilometerHour(velocityAbs);
    } else if (unit === 'kt') {
      return this.meterSec2Knots(velocityAbs);
    } else {
      return velocityAbs;
    }
  },

  vectorToDegrees: function vectorToDegrees(uMs, vMs, angleConvention) {
    if (angleConvention.endsWith('CCW')) {
      vMs = vMs > 0 ? vMs = -vMs : Math.abs(vMs);
    }
    var velocityAbs = Math.sqrt(Math.pow(uMs, 2) + Math.pow(vMs, 2));
    var velocityDir = Math.atan2(uMs / velocityAbs, vMs / velocityAbs);
    var velocityDirToDegrees = velocityDir * 180 / Math.PI + 180;
    if (angleConvention === 'bearingCW' || angleConvention === 'meteoCCW') {
      velocityDirToDegrees += 180;
      if (velocityDirToDegrees >= 360) velocityDirToDegrees -= 360;
    }
    return velocityDirToDegrees;
  },

  meterSec2Knots: function meterSec2Knots(meters) {
    return meters / 0.514;
  },

  meterSec2kilometerHour: function meterSec2kilometerHour(meters) {
    return meters * 3.6;
  }
});

L.Map.mergeOptions({
  positionControl: false
});

L.Map.addInitHook(function () {
  if (this.options.positionControl) {
    this.positionControl = new L.Control.MousePosition();
    this.addControl(this.positionControl);
  }
});

L.control.velocity = function (options) {
  return new L.Control.Velocity(options);
};

L.ParticleLayer = (L.Layer ? L.Layer : L.Class).extend({

  options: {
    displayValues: true,
    displayOptions: {
      velocityType: 'Global Wind',
      displayPosition: 'bottomleft',
      displayEmptyString: 'No wind data'
    },
    maxVelocity: 10,
    data: null,
    zindex: 0,
    magnification: 1, //画布缩放比例
    color: ["rgba(250,250,250,0.9)"],
    velocityScale: 0.005, //步长，流动速度(尽量不变)
    globalAlpha: 0.90, //全局透明度
    particleAge: 30, //粒子生命周期
    particleMultiplier: 350, //密度
    frame_rate: 30, //刷新率
    lineWidth: 1, //线宽
    type: "wind", //wind wavedir rwavedir current rcurrent
  },

  _map: null,
  _canvasLayer: null,
  _windy: null,
  _context: null,
  _timer: 0,
  _mouseControl: null,

  initialize: function initialize(options) {
    L.setOptions(this, options);
  },

  onAdd: function onAdd(map) {
    this._canvasLayer = L.canvasLayer(this.options).delegate(this);
    this._canvasLayer.addTo(map);
    this._map = map;
  },

  onRemove: function onRemove(map) {
    this._destroyWind();
  },

  setData: function setData(data) {
    this.options.data = data;
    try {
      if (this._windy) {
        this._windy.setData(data);
        this._clearAndRestart();
      }
      this.fire('load');
    } catch (e) {
      console.log(e);
    }
  },

  imgCanvas: document.createElement("canvas"), //提前创建存放图像数据的canvas
  imgData: null, //读取到内存中的图像数据
  _setdata: function _setdata(isGlobal) {
    var self = this, map = this._map;
    let referDrawTime = Config.time.dataTimeUTC,
      referDrawTime3 = Config.time.dataTime,
      curDT = new Date(Config.time.DateObj?.toString()),
      // referDrawTime2 = new Date(curDT.setHours(curDT.getHours() / 3 | 0)).FormatUTC("YYYYMMDDhh"),
      dataPath = Config.urls.dataPath, rdataPath = Config.urls.rdataPath;

    let range = {
      wind: Config.rasterHeader.current,
      waveDir: Config.rasterHeader.global,
      current: Config.rasterHeader.current,
      rwaveDir: Config.rasterHeader?.rwave ?? {},
      rtpwaveDir: Config.rasterHeader?.rtpwave ?? {},
      rcurrent: Config.rasterHeader?.current ?? {},
    }[self.options.type];


    if (self.options.type == "wind") {
      self.texturepath = `${url_gg}/output/${Config.queryTime}/wind_G_full.png`;
      // let wjj = Config.queryTime.substring(0, 8);
      // let imgName = Config.queryTime.substring(8, 10);
      // self.texturepath = url + `/imgAis/wind/${wjj}/wind.${imgName}.png`
    } else if (self.options.type == "waveDir") {
      self.texturepath = `${dataPath}/${referDrawTime}/wave_G_full.png`;
    } else if (self.options.type == "current") {
      // self.texturepath = `${dataPath}/${referDrawTime}/current_hl_full.png`;
      self.texturepath = `${url_gg}/output/${Config.queryTime}/current_hl_full.png`;
    } else if (self.options.type == "rwaveDir") {
      self.texturepath = `${rdataPath}/${referDrawTime3}/wz_wave.png`;
    } else if (self.options.type == "rtpwaveDir") {
      let curRtpwave = Config.isShow.curRtpwave
      curRtpwave = curRtpwave?.[1] ? (curRtpwave[1] + "_") : ""
      self.texturepath = `${rdataPath}/${referDrawTime3}/${curRtpwave}wz_tp_wave.png`;
    } else if (self.options.type == "rcurrent") {
      // self.texturepath = `${rdataPath}/${referDrawTime3}/wz_current.png`;
      // let wjj = Config.queryTime.substring(0, 8);
      // let imgName = Config.queryTime.substring(8, 10);
      // self.texturepath = url + `/imgAis/current/${wjj}/current.${imgName}.png`
      // http://www.oceanread.com/output/2023111500/current_hl_full.png
      self.texturepath = `${url_gg}/output/${Config.queryTime}/current_hl_full.png`;
    }

    let startLon = range.startLon;
    let startLat = range.startLat;
    let endLon = range.endLon;
    let endLat = range.endLat;
    let scale = range.scale;
    let scaleStr = range.scaleStr;

    // console.log(startLon,startLat,scale);

    if (!Config.ImgData["partcile" + self.texturepath]) {
      self.ImgData = new Image();
      self.ImgData.crossOrigin = "Anonymous";
      self.ImgData.src = self.texturepath;
      Config.showLoading();
      self.ImgData.onload = function () {
        try {
          Config.hideLoading();
          self.imgCanvas.width = self.ImgData.width || self.ImgData.naturalWidth;
          self.imgCanvas.height = self.ImgData.height || self.ImgData.naturalHeight;
          var imgCtx = self.imgCanvas.getContext("2d");
          imgCtx.drawImage(self.ImgData, 0, 0);
          if (!self.ImgData.width || !self.ImgData.height) return
          var imgdat = imgCtx.getImageData(0, 0, self.ImgData.width, self.ImgData.height).data;
          var uData = new Array();
          var vData = new Array();
          var DataUV = new Array();
          var k = 0, angleInter = ["rwaveDir"].includes(self.options.type) ? 0 : 270;
          if (self.options.type == "rtpwaveDir") angleInter = 0
          for (var i = 0; i < self.ImgData.height; i++) { //90——-90
            DataUV[i] = new Array();
            for (var j = 0; j < self.ImgData.width; j++, k++) { //0——360
              var x = (i * 4) * self.ImgData.width + (j * 4);
              var r = imgdat[x];
              var g = imgdat[x + 1];
              var b = imgdat[x + 2];
              var a = imgdat[x + 3];

              let u = 0;
              let v = 0;
              if (self.options.type == "wind") {
                u = b >= 10 ? r / 10 : -(r / 10);
                v = b % 10 == 1 ? g / 10 : -(g / 10);
              } else if (["waveDir", "rwaveDir", "rtpwaveDir"].includes(self.options.type)) {
                var value_w = r + g / 100;
                value_w = value_w > 6 ? 6 : value_w;
                value_w = (value_w / 6) * 4 + 2;
                var angle_origin = b / 255 * 360;
                var angle = (angle_origin + angleInter) % 360;
                angle = angle < 0 ? angle + 360 : angle;
                angle = 360 - angle;
                angle = angle * Math.PI / 180;
                u = Math.cos(angle) * value_w * -1;
                v = Math.sin(angle) * value_w * -1;
                if (angle_origin === 0 /* || (r + g / 100) == 0 */) {
                  u = 0, v = 0
                }
                if (["rwaveDir", "rtpwaveDir"].includes(self.options.type)) {
                  if (r + g / 100 <= 0.0) u = 0, v = 0
                }
              } else if (["current", "rcurrent"].includes(self.options.type)) {
                if (a != 0) {
                  u = r / 100.0, v = g / 100.0;
                  if (b == 0) u = -u, v = -v;
                  else if (b == 1) u = -u;
                  else if (b == 10) v = -v;
                }
              }
              uData[k] = u;
              vData[k] = v;
              DataUV[i][j] = {
                u: u,
                v: v
              };
            }
          }


          var dx = scale, dy = scale;
          if (self.options.type == "wind") {
            dx = range.uscale
            dy = range.vscale
          } else if (self.options.type == "rcurrent") {
            dx = range.uscale
            dy = range.vscale
          }
          var uHeader = {
            "lo1": startLon,
            "la1": startLat,
            "dx": dx,
            "dy": dy,
            "nx": self.ImgData.width,
            "ny": self.ImgData.height,
            "nodata": -999,
            "dataType": "windU"
          }
          var vHeader = {
            "lo1": startLon,
            "la1": startLat,
            "dx": dx,
            "dy": dy,
            "nx": self.ImgData.width,
            "ny": self.ImgData.height,
            "nodata": -999,
            "dataType": "windV"
          }

          var ParticleData = [{
            header: uHeader,
            data: uData
          }, {
            header: vHeader,
            data: vData
          }]

          self.options.data = ParticleData
          if (["current", "rcurrent"].includes(self.options.type)) {
            Config.curData[self.options.type] = DataUV
          }
          Config.ImgData["partcile" + self.texturepath] = {}
          Config.ImgData["partcile" + self.texturepath].DataUV = DataUV
          Config.ImgData["partcile" + self.texturepath].ParticleData = ParticleData

          if (self._windy) {
            self._windy.setData(ParticleData)
            self._clearAndRestart()
          }
          self.fire('load')
        } catch (error) {
          console.log(error);
        }
      }

      self.ImgData.onerror = function (err) {
        try {
          Config.hideLoading();
          if (self.options.type == "wind") {
            alertInfo("当前时刻全球海风无数据", 2500, "wind");
          } else if (self.options.type == "waveDir") {
            // alertInfo("当前全球浪向无数据", 2500, "waveDir");
          } else if (self.options.type == "current") {
            if (map._zoom <= Config.mapObj.jxhzoomF) {
              alertInfo("当前时刻全球流场无数据", 5000, "no_current");
            }
          } else if (self.options.type == "rwaveDir") {
            if (!Config.isShow.onSS && map._zoom > Config.mapObj.jxhzoomF) {
              Config.mapObj.jxhzoom = 14
              map.fire("zoomend")
              return
            }
            // alertInfo("当前精细化浪向无数据", 2500, "rwaveDir");
          } else if (self.options.type == "rtpwaveDir") {
            // alertInfo("当前精细化台风浪向无数据", 2500, "rtpwaveDir");
          } else if (self.options.type == "rcurrent") {
            if (!Config.isShow.onSS && map._zoom >= Config.mapObj.jxhzoomF) {
              Config.mapObj.jxhzoom = 14
              map.fire("zoomend")
              alertInfo("当前时刻无精细化流场数据，已切换为全球流场数据", 10000, "no_rcurrent");
              return
            }
          } else {
            alertInfo("当前时刻无数据", 2500, "oth");
          }
          self._clearWind();
          self.options.data = null;
        } catch (e) { }
      }
    } else {
      if (self._windy) {
        self._windy.setData(Config.ImgData["partcile" + self.texturepath].ParticleData);
        if (["current", "rcurrent"].includes(self.options.type)) {
          Config.curData[self.options.type] = Config.ImgData["partcile" + self.texturepath].DataUV;
        }
        self._clearAndRestart();
      }
      self.fire('load');
    }
  },

  /*------------------------------------ PRIVATE ------------------------------------------*/


  onDrawLayer: function onDrawLayer(overlay, params) {
    var self = this;
    // console.log(this.options.data)
    if (!this._windy) {
      this._initWindy(this);
      return;
    }
    if (!this.options.data) {
      return;
    }
    if (this._timer) clearTimeout(self._timer);
    if (self.options.data.length === 0) {
      return;
    }
    // console.time("windy绘制")
    self.CalcurrentRegion();
    self._startWindy();
    // console.timeEnd("windy绘制")
  },

  CalcurrentRegion: function CalcurrentRegion() { /** 计算当前区域占比 */
    let self = this;
    if (["current"].includes(self.options.type)) return;
    let bound = self._map.getBounds();
    let LonSpan = self._map.distance([bound._southWest.lat, bound._southWest.lng], [bound._southWest.lat, bound._northEast.lng]),
      LatSpan = self._map.distance([bound._northEast.lat, bound._southWest.lng], [bound._southWest.lat, bound._southWest.lng]),
      MapArea = LonSpan * LatSpan

    let range = {
      wind: Config.rasterHeader.global,
      waveDir: Config.rasterHeader.global,
      current: Config.rasterHeader.global,
      rwaveDir: Config.rasterHeader?.rwave ?? {},
      rtpwaveDir: Config.rasterHeader?.rtpwave ?? {},
      rcurrent: Config.rasterHeader?.rwave ?? {},
    }[self.options.type]
    let startLon = range.startLon, startLat = range.startLat,
      endLon = range.endLon, endLat = range.endLat
    let curLonSpan = self._map.distance([startLat, startLon], [startLat, endLon]);
    let curLatSpan = self._map.distance([startLat, startLon], [endLat, startLon]);
    let curMapArea = curLonSpan * curLatSpan;
    let bili = (curMapArea / MapArea) * 3;
    bili = bili > 1 ? 1 : bili;
    if (this._windy) this._windy.setAreaOf(bili);
    // if (this._windy) this._windy.setAreaOf(1);

    //计算尺寸放大后的 粒子数量比例
    if (this._windy) this._windy.setMagnification(this.options.magnification);
  },

  _startWindy: function _startWindy() {
    var bounds = this._map.getBounds();
    var size = this._map.getSize();
    // bounds, width, height, extent
    let self = this;
    this._windy.start([
      [0, 0],
      [size.x * self.options.magnification, size.y * self.options.magnification]
    ], size.x * self.options.magnification, size.y * self.options.magnification, [
      [bounds._southWest.lng, bounds._southWest.lat],
      [bounds._northEast.lng, bounds._northEast.lat]
    ]);
  },

  _initWindy: function _initWindy(self) {
    // windy object, copy options
    var options = Object.assign({
      canvas: self._canvasLayer._canvas
    }, self.options);
    this._windy = new Windy(options);
    // prepare context global var, start drawing
    this._context = this._canvasLayer._canvas.getContext('2d');
    this._canvasLayer._canvas.classList.add("velocity-overlay");
    this.onDrawLayer();
    this._map.on('dragend', self._windy.clearCanvas);
    this._map.on('zoomstart', self._windy.clearCanvas);
    this._map.on('resize', self._clearWind);
    this._initMouseHandler();
  },

  _initMouseHandler: function _initMouseHandler() {
    if (!this._mouseControl && this.options.displayValues) {
      var options = this.options.displayOptions || {};
      options['leafletVelocity'] = this;
      this._mouseControl = L.control.velocity(options).addTo(this._map);
    }
  },

  _clearAndRestart: function _clearAndRestart() {
    this.CalcurrentRegion();
    if (this._context) this._context.clearRect(0, 0, 3000, 3000);
    if (this._windy) this._startWindy();
  },

  /**更改参数并重绘 */
  changeParamesReset: function changeParamesReset(params) {
    let self = this;
    if (params.color) {
      self.options.color = params.color;
      if (self._windy) {
        self._windy.resetColor(params.color);
      }
    }

    if (params.velocityScale) {
      self.options.velocityScale = params.velocityScale;
      if (self._windy) {
        self._windy.resetStepM(params.velocityScale);
      }
    }

    if (params.globalAlpha) {
      self.options.globalAlpha = params.globalAlpha;
      if (self._windy) {
        self._windy.resetGlobalAlpha(params.globalAlpha);
      }
    }

    if (params.particleAge) {
      self.options.particleAge = params.particleAge;
      if (self._windy) {
        self._windy.resetParticleAge(params.particleAge);
      }
    }

    if (params.particleMultiplier) {
      self.options.particleMultiplier = params.particleMultiplier;
      if (self._windy) {
        self._windy.resetNum(params.particleMultiplier);
      }
    }
    if (params.frame_rate) {
      self.options.frame_rate = params.frame_rate;
      if (self._windy) {
        self._windy.resetFrame(params.frame_rate);
      }
    }
    if (params.lineWidth) {
      self.options.lineWidth = params.lineWidth;
      if (self._windy) {
        self._windy.resetWidth(params.lineWidth);
      }
    }

    if (self._windy) {
      self._clearAndRestart();
    }
  },

  _clearWind: function _clearWind() {
    if (this._windy) this._windy.stop();
    if (this._context) this._context.clearRect(0, 0, 3000, 3000);
  },

  _destroyWind: function _destroyWind() {
    if (this._timer) clearTimeout(this._timer);
    if (this._windy) this._windy.stop();
    if (this._context) this._context.clearRect(0, 0, 3000, 3000);
    if (this._mouseControl) this._map.removeControl(this._mouseControl);
    this._mouseControl = null;
    this._windy = null;
    this._map.removeLayer(this._canvasLayer);
  },

  _changetimeredraw: function () {
    if (this._timer) clearTimeout(this._timer);
    if (this._windy) this._windy.stop();
    if (this._context) this._context.clearRect(0, 0, 3000, 3000);
    if (this._mouseControl) this._map.removeControl(this._mouseControl);
    this._mouseControl = null;
    this._setdata();
  }
});

var Windy = function Windy(params) {
  var MIN_VELOCITY_INTENSITY = params.minVelocity || 0; // velocity at which particle intensity is minimum (m/s)
  var MAX_VELOCITY_INTENSITY = params.maxVelocity || 40; // velocity at which particle intensity is maximum (m/s)
  var VELOCITY_SCALE = (params.velocityScale || 0.005) * (Math.pow(window.devicePixelRatio, 1 / 3) || 1); // scale for wind velocity (completely arbitrary--this value looks nice)
  var MAX_PARTICLE_AGE = params.particleAge || 90; // max number of frames a particle is drawn before regeneration
  var PARTICLE_LINE_WIDTH = params.lineWidth || 2; // line width of a drawn particle
  var PARTICLE_MULTIPLIER = 1 / params.particleMultiplier || 1 / 300; // particle count scalar (completely arbitrary--this values looks nice)
  var PARTICLE_REDUCTION = Math.pow(window.devicePixelRatio, 1 / 3) || 1.6; // multiply particle count for mobiles by this amount
  var FRAME_RATE = params.frame_rate || 20; // desired frames per second
  var GLOBALALPHA = params.globalAlpha || 0.9;
  var AREA_OF = 1; //区域占比(针对区域显示)
  var Magnification = 1; //绘制比例

  var defaulColorScale = ["rgb(36,104, 180)", "rgb(60,157, 194)", "rgb(128,205,193)", "rgb(151,218,168)", "rgb(198,231,181)", "rgb(238,247,217)", "rgb(255,238,159)", "rgb(252,217,125)", "rgb(255,182,100)", "rgb(252,150,75)", "rgb(250,112,52)", "rgb(245,64,32)", "rgb(237,45,28)", "rgb(220,24,32)", "rgb(180,0,35)"];
  var colorScale = params.color;

  var NULL_WIND_VECTOR = [NaN, NaN, null]; // singleton for no wind in the form: [u, v, magnitude]

  var builder;
  var grid;
  var gridData = params.data;
  var date;
  var λ0, φ0, Δλ, Δφ, ni, nj;

  var setData = function setData(data) {
    gridData = data;
  };

  // interpolation for vectors like wind (u,v,m)
  var bilinearInterpolateVector = function bilinearInterpolateVector(x, y, g00, g10, g01, g11) {
    var rx = 1 - x;
    var ry = 1 - y;
    var a = rx * ry,
      b = x * ry,
      c = rx * y,
      d = x * y;
    var u = g00[0] * a + g10[0] * b + g01[0] * c + g11[0] * d;
    var v = g00[1] * a + g10[1] * b + g01[1] * c + g11[1] * d;
    return [u, v, Math.sqrt(u * u + v * v)];
  };

  var createWindBuilder = function createWindBuilder(uComp, vComp) {
    var uData = uComp.data,
      vData = vComp.data;
    return {
      header: uComp.header,
      //recipe: recipeFor("wind-" + uComp.header.surface1Value),
      data: function data(i) {
        return [uData[i], vData[i]];
      },
      interpolate: bilinearInterpolateVector
    };
  };

  var createBuilder = function createBuilder(data) {
    var uComp = null, vComp = null, temp = null, scalar = null;
    data.forEach(function (record) {
      switch (record.header.dataType) {
        case "windU":
          uComp = record;
          break;
        case "windV":
          vComp = record;
          break;
        default:
          scalar = record;
      }
    });
    return createWindBuilder(uComp, vComp);
  };

  var buildGrid = function buildGrid(data, callback) {
    builder = createBuilder(data);
    var header = builder.header;
    λ0 = header.lo1;
    φ0 = header.la1; // the grid's origin (e.g., 0.0E, 90.0N)
    Δλ = header.dx;
    Δφ = header.dy; // distance between grid points (e.g., 2.5 deg lon, 2.5 deg lat)
    ni = header.nx;
    nj = header.ny; // number of grid points W-E and N-S (e.g., 144 x 73)

    date = new Date(header.refTime);
    date.setHours(date.getHours() + header.forecastTime);

    grid = [];
    var p = 0;
    var isContinuous = Math.floor(ni * Δλ) >= 360;
    for (var j = 0; j < nj; j++) {
      var row = [];
      for (var i = 0; i < ni; i++, p++) {
        row[i] = builder.data(p);
      }
      if (isContinuous) {
        // For wrapped grids, duplicate first column as last column to simplify interpolation logic
        row.push(row[0]);
      }
      grid[j] = row;
    }
    callback({
      date: date,
      interpolate: interpolate
    });
  };

  /**
   * Get interpolated grid value from Lon/Lat position
   * @param λ {Float} Longitude
   * @param φ {Float} Latitude
   * @returns {Object}
   */
  var interpolate = function interpolate(λ, φ) {
    if (!grid) return null;
    var i = floorMod(λ - λ0, 360) / Δλ; // calculate longitude index in wrapped range [0, 360)
    var j = (φ0 - φ) / Δφ; // calculate latitude index in direction +90 to -90
    var fi = Math.floor(i),
      ci = fi + 1;
    var fj = Math.floor(j),
      cj = fj + 1;
    var row;
    if (row = grid[fj]) {
      var g00 = row[fi];
      var g10 = row[ci];
      if (isValue(g00) && isValue(g10) && (row = grid[cj])) {
        var g01 = row[fi];
        var g11 = row[ci];
        if (isValue(g01) && isValue(g11)) {
          // All four points found, so interpolate the value.
          return builder.interpolate(i - fi, j - fj, g00, g10, g01, g11);
        }
      }
    }
    return null;
  };

  /**
   * @returns {Boolean} true if the specified value is not null and not undefined.
   */
  var isValue = function isValue(x) {
    return x !== null && x !== undefined;
  };

  /**
   * @returns {Number} returns remainder of floored division, i.e., floor(a / n). Useful for consistent modulo
   *          of negative numbers. See http://en.wikipedia.org/wiki/Modulo_operation.
   */
  var floorMod = function floorMod(a, n) {
    return a - n * Math.floor(a / n);
  };

  /**
   * Calculate distortion of the wind vector caused by the shape of the projection at point (x, y). The wind
   * vector is modified in place and returned by this function.
   */
  var distort = function distort(projection, λ, φ, x, y, scale, wind, windy) {
    var u = wind[0] * scale;
    var v = wind[1] * scale;
    var d = distortion(projection, λ, φ, x, y, windy);
    // Scale distortion vectors by u and v, then add.
    wind[0] = d[0] * u + d[2] * v;
    wind[1] = d[1] * u + d[3] * v;
    return wind;
  };

  var distortion = function distortion(projection, λ, φ, x, y, windy) {
    var τ = 2 * Math.PI;
    // var H = Math.pow(10, -5.2);
    var H = Math.pow(10, -6);
    var hλ = λ < 0 ? H : -H;
    var hφ = φ < 0 ? H : -H;
    var pλ = project(φ, λ + hλ, windy);
    var pφ = project(φ + hφ, λ, windy);
    // Meridian scale factor (see Snyder, equation 4-3), where R = 1. This handles issue where length of 1º λ
    // changes depending on φ. Without this, there is a pinching effect at the poles.
    var k = Math.cos(φ / 360 * τ);
    return [(pλ[0] - x) / hλ / k, (pλ[1] - y) / hλ / k, (pφ[0] - x) / hφ, (pφ[1] - y) / hφ];
  };

  var createField = function createField(columns, bounds, callback) {
    /**
     * @returns {Array} wind vector [u, v, magnitude] at the point (x, y), or [NaN, NaN, null] if wind
     *          is undefined at that point.
     */
    function field(x, y) {
      var column = columns[Math.round(x)];
      return column && column[Math.round(y)] || NULL_WIND_VECTOR;
    }
    // Frees the massive "columns" array for GC. Without this, the array is leaked (in Chrome) each time a new
    // field is interpolated because the field closure's context is leaked, for reasons that defy explanation.
    field.release = function () {
      columns = [];
    };
    field.randomize = function (o) {
      // UNDONE: this method is terrible
      var x, y;
      var safetyNet = 0;
      do {
        x = Math.round(Math.floor(Math.random() * bounds.width) + bounds.x);
        y = Math.round(Math.floor(Math.random() * bounds.height) + bounds.y);
      } while (field(x, y)[2] === null && safetyNet++ < 30);
      o.x = x;
      o.y = y;
      return o;
    };
    callback(bounds, field);
  };

  var buildBounds = function buildBounds(bounds, width, height) {
    var upperLeft = bounds[0];
    var lowerRight = bounds[1];
    var x = Math.round(upperLeft[0]); //Math.max(Math.floor(upperLeft[0], 0), 0);
    var y = Math.max(Math.floor(upperLeft[1], 0), 0);
    var xMax = Math.min(Math.ceil(lowerRight[0], width), width - 1);
    var yMax = Math.min(Math.ceil(lowerRight[1], height), height - 1);
    return {
      x: x,
      y: y,
      xMax: width,
      yMax: yMax,
      width: width,
      height: height
    };
  };

  var deg2rad = function deg2rad(deg) {
    return deg / 180 * Math.PI;
  };

  var rad2deg = function rad2deg(ang) {
    return ang / (Math.PI / 180.0);
  };

  var invert = function invert(x, y, windy) {
    var mapLonDelta = windy.east - windy.west;
    var worldMapRadius = windy.width / rad2deg(mapLonDelta) * 360 / (2 * Math.PI);
    var mapOffsetY = worldMapRadius / 2 * Math.log((1 + Math.sin(windy.south)) / (1 - Math.sin(windy.south)));
    var equatorY = windy.height + mapOffsetY;
    var a = (equatorY - y) / worldMapRadius;

    var lat = 180 / Math.PI * (2 * Math.atan(Math.exp(a)) - Math.PI / 2);
    var lon = rad2deg(windy.west) + x / windy.width * rad2deg(mapLonDelta);
    return [lon, lat];
  };

  var mercY = function mercY(lat) {
    return Math.log(Math.tan(lat / 2 + Math.PI / 4));
  };

  var project = function project(lat, lon, windy) {
    // both in radians, use deg2rad if neccessary
    var ymin = mercY(windy.south);
    var ymax = mercY(windy.north);
    var xFactor = windy.width / (windy.east - windy.west);
    var yFactor = windy.height / (ymax - ymin);
    var y = mercY(deg2rad(lat));
    var x = (deg2rad(lon) - windy.west) * xFactor;
    var y = (ymax - y) * yFactor; // y points south
    return [x, y];
  };

  var interpolateField = function interpolateField(grid, bounds, extent, callback) {
    var projection = {};
    var mapArea = (extent.south - extent.north) * (extent.west - extent.east);
    var velocityScale = VELOCITY_SCALE * Math.pow(mapArea, 0.4);
    var columns = [];
    var x = bounds.x;

    function interpolateColumn(x) {
      var column = [];
      for (var y = bounds.y; y <= bounds.yMax; y += 2) {
        var coord = invert(x, y, extent);
        if (coord) {
          var λ = coord[0],
            φ = coord[1];
          if (isFinite(λ)) {
            var wind = grid.interpolate(λ, φ);
            if (wind) {
              wind = distort(projection, λ, φ, x, y, velocityScale, wind, extent);
              column[y + 1] = column[y] = wind;
            }
          }
        }
      }
      columns[x + 1] = columns[x] = column;
    }
    (function batchInterpolate() {
      var start = Date.now();
      while (x < bounds.width) {
        interpolateColumn(x);
        x += 2;
        if (Date.now() - start > 1000) {
          //MAX_TASK_TIME) {
          setTimeout(batchInterpolate, 25);
          return;
        }
      }
      createField(columns, bounds, callback);
    })();
  };

  var animationLoop;
  var animate = function animate(bounds, field) {
    function windIntensityColorScale(min, max) {
      colorScale.indexFor = function (m) {
        // map velocity speed to a style
        return Math.max(0, Math.min(colorScale.length - 1, Math.round((m - min) / (max - min) * (colorScale.length - 1))));
      };
      return colorScale;
    }

    var colorStyles = windIntensityColorScale(MIN_VELOCITY_INTENSITY, MAX_VELOCITY_INTENSITY);
    var buckets = colorStyles.map(function () {
      return [];
    });

    var particleCount = Math.round(bounds.width * bounds.height * PARTICLE_MULTIPLIER);
    particleCount = particleCount * AREA_OF;
    particleCount = Math.round(particleCount / Math.pow(Magnification, 2));

    var fadeFillStyle = "rgba(0, 0, 0, 0.97)";

    var particles = [];
    for (var i = 0; i < particleCount; i++) {
      particles.push(field.randomize({
        age: Math.floor(Math.random() * MAX_PARTICLE_AGE) + 0
      }));
    }

    function evolve() {
      buckets.forEach(function (bucket) {
        bucket.length = 0;
      });
      particles.forEach(function (particle) {
        if (particle.age > MAX_PARTICLE_AGE) {
          field.randomize(particle).age = 0;
        }
        var x = particle.x;
        var y = particle.y;
        var v = field(x, y); // vector at 当前的 position
        var m = v[2];
        if (m === null) {
          particle.age = MAX_PARTICLE_AGE; // particle has escaped the grid, never to return...
        } else {
          var xt = x + v[0];
          var yt = y + v[1];
          if (field(xt, yt)[2] !== null) {
            // Path from (x,y) to (xt,yt) is visible, so add this particle to the appropriate draw bucket.
            particle.xt = xt;
            particle.yt = yt;
            buckets[colorStyles.indexFor(m)].push(particle);
          } else {
            // Particle isn't visible, but it still moves through the field.
            particle.x = xt;
            particle.y = yt;
          }
        }
        particle.age += 1;
      });
    }

    var g = params.canvas.getContext("2d");
    g.lineWidth = PARTICLE_LINE_WIDTH * Magnification;
    // g.fillStyle = fadeFillStyle;

    function draw() {
      // Fade existing particle trails.
      var prev = "lighter";
      g.globalCompositeOperation = "destination-in";
      g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
      g.globalCompositeOperation = prev;
      g.globalAlpha = GLOBALALPHA;

      buckets.forEach(function (bucket, i) { // Draw new particle trails.
        if (bucket.length > 0) {
          g.beginPath();
          g.strokeStyle = colorStyles[i];
          if (["waveDir", "rwaveDir", "rtpwaveDir"].includes(params.type)) {
            g.lineWidth = (8 + i * 0.75) * Magnification;
          }
          bucket.forEach(function (particle) {
            g.moveTo(particle.x, particle.y);
            g.lineTo(particle.xt, particle.yt);
            particle.x = particle.xt;
            particle.y = particle.yt;
          });
          g.stroke();
        }
      });
    }

    var then = Date.now();
    (function frame() {
      animationLoop = requestAnimationFrame(frame);
      var now = Date.now();
      var delta = now - then;
      if (delta > (1000 / FRAME_RATE)) {
        then = now - delta % (1000 / FRAME_RATE);
        evolve();
        draw();
      }
    })();
  };

  var start = function start(bounds, width, height, extent) {

    var mapBounds = {
      south: deg2rad(extent[0][1]),
      north: deg2rad(extent[1][1]),
      east: deg2rad(extent[1][0]),
      west: deg2rad(extent[0][0]),
      width: width,
      height: height
    };

    stop();

    buildGrid(gridData, function (grid) { // build grid
      interpolateField(grid, buildBounds(bounds, width, height), mapBounds, function (bounds, field) { // interpolateField
        // animate the canvas with random points
        windy.field = field;
        animate(bounds, field);
      });
    });
  };

  var stop = function stop() {
    if (windy.field) windy.field.release();
    if (animationLoop) cancelAnimationFrame(animationLoop);
  };

  /**设置尺寸夸大比例*/
  var setMagnification = function setMagnification(value) {
    Magnification = value;
  };

  /**设置区域占比*/
  var setAreaOf = function setAreaOf(value) {
    AREA_OF = value;
  };

  /**重置粒子-密度 */
  var resetNum = function resetNum(value) {
    PARTICLE_MULTIPLIER = 1 / value;
  };

  /**重置粒子-宽度 */
  var resetWidth = function resetWidth(value) {
    PARTICLE_LINE_WIDTH = value;
  };

  /**重置粒子-颜色 */
  var resetColor = function resetColor(value) {
    colorScale = value;
  };

  /**重置粒子-速率 */
  var resetFrame = function resetFrame(value) {
    FRAME_RATE = value;
  };

  /**重置粒子-步长 */
  var resetStepM = function resetStepM(value) {
    VELOCITY_SCALE = value;
  };

  /**重置粒子-周期 */
  var resetParticleAge = function resetParticleAge(value) {
    MAX_PARTICLE_AGE = value;
  };

  /**重置全局透明度 */
  var resetGlobalAlpha = function resetGlobalAlpha(value) {
    GLOBALALPHA = value;
  };

  var clearCanvas = function clearCanvas() {
    stop();
    if (params.canvas) params.canvas.height = params.canvas.height;
  };

  var windy = {
    params: params,
    start: start,
    stop: stop,
    createField: createField,
    interpolatePoint: interpolate,
    setData: setData,
    clearCanvas: clearCanvas,
    setAreaOf: setAreaOf,
    setMagnification: setMagnification,
    resetNum: resetNum,
    resetWidth: resetWidth,
    resetColor: resetColor,
    resetFrame: resetFrame,
    resetStepM: resetStepM,
    resetParticleAge: resetParticleAge,
    resetGlobalAlpha: resetGlobalAlpha,
  };

  return windy;
};

if (!window.cancelAnimationFrame) {
  window.cancelAnimationFrame = function (id) {
    clearTimeout(id);
  };
}

export default function (options) {
  return new L.ParticleLayer(options);
};